#!/usr/bin/env python
""" AnyBackup is a windows specific backup program to keep multiple volumes in sync with multiple volumes
    Copyright (C) 2014  Andrew Hopkinson

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    Exported Classes:

    messagingMixin -- a small mixin to support setting and writing to a messaging queue
    pathMixin -- provides a path building function, for reasons unknown os.path.join sometimes returns nonsensical results
    backupMixin -- a mixin to do core backup functionality, shared between the backup manager and the server script
    """

import os
import time
from enum import AEnums
import math
import hashlib
import sys
import re
import logging
import traceback

logger = logging.getLogger('AnyBackup')


def print_timing(func):
    def wrapper(*arg):
        t1 = time.time()
        res = func(*arg)
        t2 = time.time()
        print '%s took %0.3f ms' % (func.func_name, (t2 - t1) * 1000.0)
        return res

    return wrapper


class messagingMixin:
    def setMessagingQueue(self, queue):
        global messagingQueue
        messagingQueue = queue

    def sendMessage(self, message):
        if messagingQueue:
                messagingQueue.put(message, 1)
        else:
                print message


class pathMixin:
    def toFullPath(self, path):
        if sys.platform == 'win32':
            path = path.replace('\\\\','\\')
            return u'\\\\?\\'+path
        return path

    def fullPath(self, *args):
        return self.toFullPath(self.createPath(*args))

    def createPath(self, *args):
        pathStr = u''
        for piece in args:
                if pathStr:
                        pathStr += os.sep
                pathStr += piece
        while os.sep*2 in pathStr:
            pathStr = pathStr.replace(os.sep*2, os.sep)
        return pathStr


class backupMixin(pathMixin):

    def getModifiedTime(self, path):
        """
        Gets the last modified time of a given path, rounds it up to the nearest second, this bit of hackery
         is to compensate for the fact that windows only has a resolution of 2 seconds for modification times
         where as unix has a resolution in fractions of a second which can lead to mismatches if you're backing
         up off of a unix sourced network share

         path -- the path to get the last modified time for
        """
        mtime = int(math.floor(os.path.getmtime(path)))
        if mtime % 2:
            mtime += 1
        return mtime

    def createDirRef(self, drive, path, dirName, root):
        """
        drive -- the drive this directory belongs to
        path -- the filepath to this directory
        dirName -- the name of this directory
        root -- the root directory of this path (if backing up from a content drive where
                 only certain directories are being backed up)
        """
        fd = {
            AEnums.NAME: os.path.basename(dirName),
            AEnums.PATH: os.path.dirname(path),
            AEnums.PREFIX: drive[AEnums.NAME] + ':',
            AEnums.DIR: True,
            AEnums.VOLNAME: drive[AEnums.NAME],
            AEnums.SERIAL: drive[AEnums.SERIAL],
            AEnums.SIZE: 0,
            AEnums.MTIME: self.getModifiedTime(dirName),
            AEnums.PARENT: '',
            AEnums.SETNAME: drive[AEnums.SETNAME],
        }
        m = hashlib.md5()
        m.update(fd[AEnums.NAME].encode('UTF-16'))
        m.update(fd[AEnums.PATH].encode('UTF-16'))
        m.update(fd[AEnums.VOLNAME])
        m.update(fd[AEnums.SERIAL])
        m.update(fd[AEnums.SETNAME])
        if root:
            m.update(AEnums.ROOT)
        fileID = m.hexdigest()
        fd[AEnums.ID] = fileID
        return fd

    def createFileRef(self, drive, path, dirName, foundFile):
        """
        drive -- the drive this file belongs to
        path -- the path to this file
        dirName -- the directory this file resides in
        foundFile -- the name of this file
        """
        if AEnums.SEP in drive:
            path = path.replace(os.sep, drive[AEnums.SEP])
        newFile = {
            AEnums.NAME: foundFile,
            AEnums.PATH: path,
            AEnums.PREFIX: drive[AEnums.NAME] + ':',
            AEnums.DIR: False,
            AEnums.SIZE: float(os.path.getsize(self.fullPath(dirName, foundFile))) / float(1024),
            AEnums.MTIME: self.getModifiedTime(self.fullPath(dirName, foundFile)),
            AEnums.VOLNAME: drive[AEnums.NAME],
            AEnums.SERIAL: drive[AEnums.SERIAL],
            AEnums.SETNAME: drive[AEnums.SETNAME],
        }
        m = hashlib.md5()
        m.update(newFile[AEnums.NAME].encode('UTF-16'))
        m.update(newFile[AEnums.PATH].encode('UTF-16'))
        m.update(newFile[AEnums.VOLNAME])
        m.update(newFile[AEnums.SERIAL])
        m.update(newFile[AEnums.SETNAME])
        fileID = m.hexdigest()
        newFile[AEnums.ID] = fileID
        return newFile

    def shouldProcess(self, dirName, foundFile, skip, ext):
        """
        Validates that a file satisfies the skip list and the valid extension list

        dirName -- the directory of the file
        foundFile -- the name of the file
        skip -- a regular expression of files to skip
        ext -- a regular expression of valid extensions
        """
        if not os.access(self.createPath(dirName, foundFile), os.R_OK):
            return False
        if skip:
            if re.search(skip, foundFile):
                return False
        if not os.path.isdir(self.createPath(dirName, foundFile)):
            if ext:
                if not re.search(ext, foundFile):
                    return False
        return True

    def getIndexFiles(self, dirName):
        """
        Attempts to return a list of files in a directory, ignores directories it cannot list,
        but does log an error

        dirName -- directory to get a list of files for
        """
        try:
            files = os.listdir(unicode(dirName))
        except:
            logger.error(traceback.format_exc())
            files = []
        return files

    def compileValidExt(self, validext):
        """
        Combines all extensions into one regular expression and then creates a compiled regex object

        validext -- a list of extensions (ex ['zip', 'avi' ..])
        """
        if validext:
            validext = '|'.join(validext)
            validext = "\.(" + validext + ")$"
            validext = re.compile(str(validext), re.IGNORECASE)
        else:
            validext = None
        return validext

    def compileSkipList(self, skip):
        """
        Combines all skip regexes into one and then creates a compiled regex object

        skip -- a list of regexes to skip (ex ['.ds_store', 'thumbs.db'])
        """
        skip = '|'.join(skip)
        if skip:
            skip = re.compile(str(skip), re.IGNORECASE)
        else:
            skip = None
        return skip
